Fix uninitialized and leaked libRoadrunner instance handle - #425
Open
drbergman wants to merge 3 commits into
Open
Fix uninitialized and leaked libRoadrunner instance handle#425drbergman wants to merge 3 commits into
drbergman wants to merge 3 commits into
Conversation
Two bugs, both in the lifetime of the RoadRunner instance behind
RoadRunnerIntracellular::rrHandle.
A cell that changes type never gets its model started. Phenotype::operator= deep-copies
the intracellular model through clone(), but clone() returned a model with no RoadRunner
instance -- only start() creates one, and start() was called from exactly two places in
the engine, Cell::divide() and create_cell( Cell_Definition& ).
Cell::convert_to_cell_definition() assigns a phenotype and did not, so every cell
transformation left the cell holding whatever the recycled heap block contained.
rrHandle is declared with no initializer, so that is undefined memory; on glibc the block
comes back with its previous bytes, on Darwin it comes back zeroed.
Nothing was ever freed. freeRRInstance appears nowhere in the tree and the class declared
no destructor, so the implicit one destroyed the STL members and leaked the instance
behind the raw handle. Every cell death and every conversion orphaned one.
Measured with this branch's ode_energy sample extended with a second model-carrying cell
type, transformations in both directions, and matched birth/death holding the population
near 144 agents:
conversions enabled before: crash 8/8 after: clean 6/6
(5 churn seeds gave 4x SIGSEGV and 1x SIGABRT out of libmalloc)
death only, 2880 min before: 530.7/578.6/573.8 MB after: 105.6/108.6/101.0 MB
no conversion/death before: 127.9 MB after: 121.4 MB (both exit 0)
The mixed SIGSEGV/SIGABRT pattern matches what an HPC user of this addon reported. Under
lldb the faulting frame is get_parameter_value() with rrHandle == 0x0 and a valid
sbml_filename, inside an OpenMP region.
The fix keeps the instance's whole lifetime inside the addon:
clone() calls start() on the new model, after the field assignments it already does,
since start() loads sbml_filename. It therefore hands back a usable model. This matches
MaBoSS and dFBA, which both re-initialize their engines in their own copy constructors
-- dFBAIntracellular::start() is empty precisely because of it. Requiring callers to
call start() afterwards was libRoadrunner's requirement leaking into core.
A destructor frees rrHandle and result. freeRRInstance and freeRRCData both accept null,
verified against the shipped library, so no guards are needed.
rrHandle gets an in-class initializer, which covers the pointer constructor too since it
has no mem-initializer list.
Because clone() is now self-sufficient, core/PhysiCell_cell.cpp only loses code: both
existing start() calls are redundant and are removed. That also closes the remaining hole
-- the no-argument create_cell() reaches Cell::Cell(), which clones cell_defaults.phenotype
and previously left it unstarted.
The copy operations are deleted, since with an owning destructor the implicit ones would
shallow-copy the handle and double-free.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
drbergman
marked this pull request as ready for review
August 7, 2026 12:26
Keeps only the part that is not obvious from the code -- that start() has to run after the field assignments, because it loads sbml_filename. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
get_parameter_value() and set_parameter_value() index species_result_column_index with operator[], the inserting form. An unknown species name silently resolves to column 0, so a typo -- or a model whose column map was never populated -- reads, and writes, a different species than the caller named. A read also mutates the map. Both accessors now look the species up with find() and, on a miss, report and exit. Exiting rather than warning is deliberate: there is no value to return and nothing sensible to write, so continuing would feed a fabricated number into the model and every step after it. validate_SBML_species() already exits for exactly this condition at setup; it just cannot cover these calls, because it only validates the mappings declared in the XML and custom code calls the accessors directly with its own strings. The lookup also moved ahead of getFloatingSpeciesConcentrations(), so a miss no longer allocates a concentration vector it then discards. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
drbergman
commented
Aug 7, 2026
Collaborator
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fix uninitialized and leaked libRoadrunner instance handle
RoadRunnerIntracellular::rrHandleis declared with no initializer,clone()returns a model with no RoadRunner instance, and nothing ever frees one.A cell that changes type gets a handle that was never created. Only
start()creates the instance, and it is called from exactly two places —Cell::divide()andcreate_cell( Cell_Definition& ).Cell::convert_to_cell_definition()assigns a phenotype and does not, so every cell transformation leaves the cell passing indeterminate memory to the RoadRunner C API.Nothing is ever freed.
freeRRInstanceappears nowhere in the tree and the class declares no destructor, so every cell death and every conversion permanently orphans one instance.The fix
clone()callsstart(), so it returns a usable model. This matches MaBoSS and dFBA, which both initialize in their own copy constructors —dFBAIntracellular::start()is empty for exactly that reason.rrHandleandresult.rrHandlegets an in-class initializer, which covers the pointer-taking constructor too.Because
clone()is now self-sufficient, both existingstart()calls incore/PhysiCell_cell.cppare redundant and removed.Verification
The stock
ode_energysample exercises neither bug — it has no cell death and no type conversion. Extended with a second cell definition carrying the same<intracellular>block, transformations in both directions, and matched birth/death holding ~144 agents:Also included: unknown SBML species
get_parameter_value()/set_parameter_value()indexedspecies_result_column_indexwithoperator[], so an unknown species silently resolved to column 0 — reading, or writing, a different species than the caller named. Both now usefind()and exit, asvalidate_SBML_species()already does at setup. See this comment.